0%

kill query threadId与kill [connection] threadId

在MySQL中有两个kill命令:一个是kill query +线程id,表示终止这个线程中正在执行的语句;一个是kill [connection] +线程id,表示断开这个线程的连接,如果这个线程有语句正在执行,是要先停止正在执行的语句的。

有时使用了kill命令,却没能断开这个连接。再执行show processlist命令,看到这条语句的Command列显示的是Killed。

大多数情况下,kill query/connection命令是有效的。

  • 执行一个查询的过程中,发现执行时间太久,要放弃继续查询,可以用kill query命令,终止这条查询语句。
  • 语句处于锁等待的时候,直接使用kill命令。
sessionA sessionB sessionC
begin;
update t set c=c+1 where id=1;
update t set c=c+1 where id=1;
(blocked)
ERROR 1317(70100):Query execution was interrupted kill query thread_id_B;

收到kill以后,线程做什么

当对一个表做增删改查操作时,会在表上加MDL读锁。sessionB虽然处于blocked状态,但还是拿着一个MDL读锁,如果线程被kill的时候,直接终止,那之后这个MDL读锁就没机会被释放了。

kill并不是马上停止的意思,而是告诉执行线程说,这条语句已经不需要继续执行了,可以开始执行停止的逻辑了。

跟Linux的kill命令类似,kill -N pid并不是让进程直接停止,而是给进城发一个信号,然后进程处理这个信号,进入终止逻辑。只是对于MySQL的kill命令来说,不需要传信号量参数,就只有“停止”这个命令。

当用户执行kill query Thread_id_B时,MySQL里处理kill命令的线程做了两件事:

  1. 把sessionB的运行状态改成THD::KILL_QUERY(将变量killed赋值为THD::KILL_QUERY);
  2. 给sessionB的执行线程发一个信号。

    sessionB处于锁等待状态,如果只是把sessionB的线程状态设置THD::KILL_QUERY,线程B并不知道这个状态变化,还是会继续等待。发一个信号的目的,就是让sessionB退出等待,来处理这个THD::KILL_QUERY状态。

  • 一个语句执行过程中有多处“埋点”,在这些“埋点”的地方判断线程状态,如果发现线程状态时THD::KILL_QUERY,才开始进入语句终止逻辑;
  • 如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到“埋点”处;
  • 语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的。

kill query无效

MySQL-数据库异常状态判断 中innodb_thread_concurrency参数讲解,不够用例子。

线程没有执行到判断线程状态的逻辑

执行set global innodb_thread_concurrency=2,将InnoDB的并发线程上限数设置为2;

sessionA sessionB sessionC sessionD sessionE
select sleep(100) from t; select sleep(100) from t;
select * from t;
(blocked)
kill query C;
ERROR 2013(HY000):Lost connection to MySQL server during query kill C;
  1. sessionC执行的时候被堵住了;
  2. 但是sessionD执行的kill query C命令却没什么效果;
  3. 直到sessionE执行了kill connection命令,才断开了sessionC的连接,提示”Lost connection to MySQL server during query”;
  4. 但是这时,如果在sessionE中执行show processlist,展示的sessionC对应的线程的Command列显示的是Killed。客户端虽然断开了连接,但实际上服务端上这条语句还在执行过程中。

对比:
在实现上,例子1中update语句等行锁时,使用的是pthread_cond_timedwait函数,这个等待状态可以被唤醒。但是例子2,sessionC对应的线程等待逻辑是这样的,每10毫秒判断一下是否可以进入InnoDB执行,如果不行,就调用nanosleep函数进入sleep状态。

虽然sessionC对应的线程的状态已经被设置成了KILL_QUERY,但是在这个等待进入InnoDB的循环过程中,并没有去判断线程的状态,因此根本不会进入终止逻辑阶段。

当sessionE执行kill connection命令时:

  1. 把sessionC对应的线程状态设置为KILL_CONNECTION;
  2. 关掉sessionC对应线程的网络连接。因为这个操作,可以看到此时sessionC收到了断开连接的提示。

kill connection本质上只是把客户端的sql断开,后面的执行流程还是要走kill query的。另外执行show processlist时,有一个特别的逻辑:如果一个线程的状态时KILL_CONNECTION,就把Command列显示成killed。

即使是客户端退出了,这个线程的状态仍然是在等待中。
只有等待满足进入InnoDB的条件后,sessionC的查询语句继续执行,然后才有可能判断到线程状态已经变成了kill_query或者kill_connection,再进入终止逻辑阶段。

其他情况:
IO压力过大,读写IO的函数一直无法返回,导致不能及时判断线程的状态。

终止逻辑耗时较长

从show processlist结果上看是Command=killed,需要等到终止逻辑完成,语句才算真正完成。

  1. 超大事务执行期间被kill。这时,回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长。
  2. 大查询回滚。如果查询过程中生成了比较大的临时文件,加上此时文件系统压力大,删除临时文件可能需要等待IO资源,导致耗时较长。
  3. DDL命令执行到最后阶段,如果被kill,需要删除中间过程的临时文件,也可能受IO资源影响耗时较久。

解决方案

要kill掉一个线程,涉及到后端的很多操作。
发送kill命令的客户端,并没有强行停止目标线程的执行,只是设置了个状态,并唤醒对应的线程。而被kill的线程,需要执行到判断状态的“埋点”,才会开始进入终止逻辑阶段。并且,终止逻辑本身也是需要耗费时间的。导致出现“kill不掉”的情况。

如果发现一个线程处于killed状态,可以通过影响系统环境,让整个killed状态尽快结束。

  • InnoDB并发度的问题,可以临时调大innodb_thread_concurrency的值,或者停掉别的线程,让出位子给这个线程执行。
  • 回滚逻辑由于受到IO资源限制执行得比较慢,通过减少系统压力让它加速。

其他情况只能等待流程自己完成。

Ctrl+C

直接在客户端通过Ctrl+C命令,不会直接终止线程。
在客户端的操作只能操作到客户端的线程,客户端和服务端只能通过网络交互,是不可能直接操作服务端线程的。

由于MySQL是停等协议,所以这个线程执行的语句还没有返回的时候,再往这个连接里面继续发命令也是没有用的。实际上,执行Ctrl+C的时候,是MySQL客户端另外启动了一个连接,然后发送一个kill query命令。

关于客户端的其他误解

-A参数 如果库里面的表特别多,连接就会很慢

有些线上的库,会包含很多表。每次用客户端连接都会卡在界面上:

mysql -h127.0.0.1 -uu1 -pp1 db1
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

如果db1这个库里表很少的话,连接起来就会很快,可以很快进入输入命令的状态。误认为是表的数目影响了连接性能。

每个客户端在和服务端建立连接的时候,需要做的事情就是TCP握手、用户校验、获取权限。但这几个操作,显然跟库中表的个数无关。

实际上,当使用默认参数连接的时候,MySQL客户端会提供一个本地库名和表名补全的功能。为了实现这个功能,客户端连接成功后,需要多做一些操作:

  1. 执行show database;
  2. 切到db1库,执行show tables;
  3. 把这两个命令的结果用于构建一个本地的哈希表。

在这些操作中,最花时间的就是第三步在本地构建哈希表的操作,所以,当一个库中的表个数非常多的时候,这一步会花比较长的时间。
感知到的连接过程慢,并不是连接慢,也不是服务端慢,而是客户端慢。

自动补全的效果就是,在输入库名或者表名的时候,输入前缀,可以使用Tab建自动补全表名或者显示提示。在连接命令中加上-A,可以关掉这个自动补全的功能。

-quick参数

除了加-A以外,加-quick(简写为-q)参数,也可以跳过这个阶段。

设置了这个参数可能会降低服务端的性能。

MySQL客户端发送请求后,接收服务端返回结果的方式有两种:

  1. 一种是本地缓存,也就是在本地开一片内存,先把结果存起来。如果用API开发,对应的就是mysql_store_result方法。
  2. 一种是不缓存,读一个处理一个。如果用API开发,对应的就是mysql_use_result方法。

MySQL客户端默认采用第一种方式,而如果加上-quick参数,就会使用第二种不缓存的方式。采用不缓存的方式时,如果本地处理的很慢,就会导致服务端发送结果被阻塞,因此会让服务端变慢。

quick参数的意思是让客户端变快:

  1. 跳过表名自动补全功能。
  2. mysql_store_result需要申请本地内存来缓存查询结果,如果查询结果太大,会耗费较多的本地内存,可能会影响客户端本地机器的性能。
  3. 不会把执行命令记录到本地的命令历史文件。